هدف این پروژه طراحی یک سیستم هوشمند تشخیص کمککننده (Decision Support System) برای شناسایی بیماری تیروئید است. این سیستم بر اساس ویژگیهای دموگرافیک (سن، جنسیت) و نتایج آزمایشگاهی (سطح هورمونهای تیروئید)، بیماران را از افراد سالم متمایز میکند.
اهمیت پزشکی و کاربردی:
بیماری تیروئید یکی از شایعترین اختلالات غدد درونریز است که میلیونها نفر در سراسر جهان را تحت تأثیر قرار میدهد. تیروئید غدهای است به شکل پروانه در قسمت جلویی گردن که هورمونهای تنظیمکننده متابولیسم بدن را تولید میکند.
انواع اختلالات تیروئید:
چالشهای تشخیص سنتی:
ارزش افزوده مدل یادگیری ماشین:
library(tidyverse) # دستکاری و مصورسازی دادهها
library(caret) # یادگیری ماشین و آموزش مدلها
library(rpart) # درخت تصمیم (Decision Trees)
library(rpart.plot) # مصورسازی درخت تصمیم
library(randomForest) # جنگل تصادفی (Random Forest)
library(nnet) # شبکههای عصبی (Neural Networks)
library(class) # الگوریتم KNN
library(corrplot) # ماتریس همبستگی
library(skimr) # خلاصهسازی آماری
library(ggplot2) # مصورسازی پیشرفته
library(DT) # جداول تعاملیشرح تفصیلی نقش هر کتابخانه:
۱. tidyverse: یک اکوسیستم منسجم از کتابخانهها شامل:
dplyr: دستکاری دادهها (فیلتر، انتخاب، گروهبندی، خلاصهسازی)
ggplot2: مصورسازی دادهها بر اساس Grammar of Graphics
tidyr: پاکسازی و تبدیل ساختار دادهها
readr: خواندن سریع فایلهای متنی
این کتابخانه فلسفه یکپارچه “Tidy Data” را پیادهسازی میکند که در آن هر متغیر یک ستون، هر مشاهده یک سطر، و هر نوع واحد مشاهده یک جدول است.
۲. caret (Classification And REgression Training):
۳. rpart (Recursive Partitioning And Regression Trees):
۴. randomForest:
۵. nnet (Neural Networks):
۶. class (K-Nearest Neighbors):
۷. corrplot:
۸. skimr:
summary() پایه
# بارگذاری دادهها با مدیریت مقادیر گمشده
df <- read.csv("thyroid_disease.csv",
na.strings = c("", "NA", "?", "NaN", " "))
# بررسی ابعاد
dim(df)## [1] 4149 29
## [1] "X......." "ThryroidClass"
## [3] "patient_age" "patient_gender"
## [5] "presc_thyroxine" "queried_why_on_thyroxine"
## [7] "presc_anthyroid_meds" "sick"
## [9] "pregnant" "thyroid_surgery"
## [11] "radioactive_iodine_therapyI131" "query_hypothyroid"
## [13] "query_hyperthyroid" "lithium"
## [15] "goitre" "tumor"
## [17] "hypopituitarism" "psych_condition"
## [19] "TSH_measured" "TSH_reading"
## [21] "T3_measured" "T3_reading"
## [23] "T4_measured" "T4_reading"
## [25] "thyrox_util_rate_T4U_measured" "thyrox_util_rate_T4U_reading"
## [27] "FTI_measured" "FTI_reading"
## [29] "ref_src"
توضیح پارامترهای بارگذاری:
آرگومان na.strings: این پارامتر بسیار مهم
است و مشخص میکند کدام مقادیر در فایل CSV باید به عنوان “مقدار گمشده”
(Missing Value) تلقی شوند:
““: سلولهای خالی
“NA”: متن NA (رایج در R)
“?”: علامت سوال (رایج در دیتاستهای UCI)
“NaN”: Not a Number
” “: فاصله خالی (Space)
اگر این مقادیر را به درستی مشخص نکنیم، R آنها را به عنوان داده واقعی در نظر میگیرد که میتواند نتایج مدل را کاملاً خراب کند.
## 'data.frame': 4149 obs. of 29 variables:
## $ X....... : chr "2635" "1995" "2215" "133" ...
## $ ThryroidClass : chr "Negative" "negative" "negative" "Negative" ...
## $ patient_age : chr "29, " "50, " "70, " "55, " ...
## $ patient_gender : int 1 1 1 1 1 1 NA 1 1 0 ...
## $ presc_thyroxine : int 0 0 1 0 0 0 NA 0 0 0 ...
## $ queried_why_on_thyroxine : int 0 0 NA 0 0 0 NA 0 0 0 ...
## $ presc_anthyroid_meds : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ sick : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ pregnant : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ thyroid_surgery : int 0 1 0 0 0 0 NA 0 0 0 ...
## $ radioactive_iodine_therapyI131: int 0 0 0 0 0 0 NA 0 0 0 ...
## $ query_hypothyroid : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ query_hyperthyroid : int 0 0 0 0 1 0 NA 0 0 0 ...
## $ lithium : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ goitre : int 0 0 0 NA NA 0 NA 0 0 0 ...
## $ tumor : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ hypopituitarism : int 0 0 0 0 0 0 NA 0 0 0 ...
## $ psych_condition : int 0 0 NA 0 0 0 NA 0 0 0 ...
## $ TSH_measured : int 1 1 1 1 1 1 NA 1 1 NA ...
## $ TSH_reading : num 2.1 76 8.5 1.8 0.15 2.4 NA 1.6 1.9 0.84 ...
## $ T3_measured : int 1 1 0 0 0 1 NA 0 1 1 ...
## $ T3_reading : num 2.2 0.5 NA NA NA 1.9 NA NA 2 1.3 ...
## $ T4_measured : int 1 1 1 1 1 1 NA 1 1 1 ...
## $ T4_reading : num 101 22 NA 84 133 170 NA 67 109 66 ...
## $ thyrox_util_rate_T4U_measured : int 1 1 1 1 1 1 NA 1 0 1 ...
## $ thyrox_util_rate_T4U_reading : num 0.79 1.12 1.05 NA 0.99 1.23 NA 0.71 NA 1.17 ...
## $ FTI_measured : int 1 1 1 1 1 1 NA 1 0 1 ...
## $ FTI_reading : num 129 19 121 88 135 138 NA 95 NA 56 ...
## $ ref_src : chr "other" "other" "other" "other" ...
## X....... ThryroidClass patient_age patient_gender
## Length:4149 Length:4149 Length:4149 Min. :0.000
## Class :character Class :character Class :character 1st Qu.:0.000
## Mode :character Mode :character Mode :character Median :1.000
## Mean :0.656
## 3rd Qu.:1.000
## Max. :1.000
## NA's :573
## presc_thyroxine queried_why_on_thyroxine presc_anthyroid_meds
## Min. :0.0000 Min. :0.00000 Min. :0.00000
## 1st Qu.:0.0000 1st Qu.:0.00000 1st Qu.:0.00000
## Median :0.0000 Median :0.00000 Median :0.00000
## Mean :0.1231 Mean :0.01282 Mean :0.01114
## 3rd Qu.:0.0000 3rd Qu.:0.00000 3rd Qu.:0.00000
## Max. :1.0000 Max. :1.00000 Max. :1.00000
## NA's :558 NA's :561 NA's :557
## sick pregnant thyroid_surgery
## Min. :0.00000 Min. :0.00000 Min. :0.00000
## 1st Qu.:0.00000 1st Qu.:0.00000 1st Qu.:0.00000
## Median :0.00000 Median :0.00000 Median :0.00000
## Mean :0.03907 Mean :0.01452 Mean :0.01395
## 3rd Qu.:0.00000 3rd Qu.:0.00000 3rd Qu.:0.00000
## Max. :1.00000 Max. :1.00000 Max. :1.00000
## NA's :566 NA's :568 NA's :565
## radioactive_iodine_therapyI131 query_hypothyroid query_hyperthyroid
## Min. :0.00000 Min. :0.00000 Min. :0.00000
## 1st Qu.:0.00000 1st Qu.:0.00000 1st Qu.:0.00000
## Median :0.00000 Median :0.00000 Median :0.00000
## Mean :0.01619 Mean :0.06001 Mean :0.06199
## 3rd Qu.:0.00000 3rd Qu.:0.00000 3rd Qu.:0.00000
## Max. :1.00000 Max. :1.00000 Max. :1.00000
## NA's :567 NA's :566 NA's :568
## lithium goitre tumor hypopituitarism
## Min. :0.00000 Min. :0.0000 Min. :0.00000 Min. :0.00000
## 1st Qu.:0.00000 1st Qu.:0.0000 1st Qu.:0.00000 1st Qu.:0.00000
## Median :0.00000 Median :0.0000 Median :0.00000 Median :0.00000
## Mean :0.00475 Mean :0.0092 Mean :0.02593 Mean :0.00028
## 3rd Qu.:0.00000 3rd Qu.:0.0000 3rd Qu.:0.00000 3rd Qu.:0.00000
## Max. :1.00000 Max. :1.0000 Max. :1.00000 Max. :1.00000
## NA's :567 NA's :563 NA's :562 NA's :567
## psych_condition TSH_measured TSH_reading T3_measured
## Min. :0.0000 Min. :0.0000 Min. : 0.005 Min. :0.000
## 1st Qu.:0.0000 1st Qu.:1.0000 1st Qu.: 0.500 1st Qu.:1.000
## Median :0.0000 Median :1.0000 Median : 1.400 Median :1.000
## Mean :0.0477 Mean :0.9016 Mean : 4.960 Mean :0.797
## 3rd Qu.:0.0000 3rd Qu.:1.0000 3rd Qu.: 2.700 3rd Qu.:1.000
## Max. :1.0000 Max. :1.0000 Max. :530.000 Max. :1.000
## NA's :564 NA's :561 NA's :921 NA's :562
## T3_reading T4_measured T4_reading
## Min. : 0.050 Min. :0.0000 Min. : 2.0
## 1st Qu.: 1.600 1st Qu.:1.0000 1st Qu.: 88.0
## Median : 2.000 Median :1.0000 Median :103.5
## Mean : 2.013 Mean :0.9392 Mean :108.4
## 3rd Qu.: 2.400 3rd Qu.:1.0000 3rd Qu.:124.0
## Max. :10.600 Max. :1.0000 Max. :430.0
## NA's :1299 NA's :564 NA's :785
## thyrox_util_rate_T4U_measured thyrox_util_rate_T4U_reading FTI_measured
## Min. :0.0000 Min. :0.2500 Min. :0.0000
## 1st Qu.:1.0000 1st Qu.:0.8800 1st Qu.:1.0000
## Median :1.0000 Median :0.9800 Median :1.0000
## Mean :0.8968 Mean :0.9948 Mean :0.8981
## 3rd Qu.:1.0000 3rd Qu.:1.0800 3rd Qu.:1.0000
## Max. :1.0000 Max. :2.3200 Max. :1.0000
## NA's :565 NA's :931 NA's :568
## FTI_reading ref_src
## Min. : 2.0 Length:4149
## 1st Qu.: 93.0 Class :character
## Median :107.0 Mode :character
## Mean :110.4
## 3rd Qu.:124.0
## Max. :395.0
## NA's :941
| Name | df |
| Number of rows | 4149 |
| Number of columns | 29 |
| _______________________ | |
| Column type frequency: | |
| character | 4 |
| numeric | 25 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| X……. | 1 | 1.00 | 1 | 6 | 0 | 4148 | 0 |
| ThryroidClass | 377 | 0.91 | 4 | 8 | 0 | 3 | 0 |
| patient_age | 571 | 0.86 | 3 | 4 | 0 | 93 | 0 |
| ref_src | 568 | 0.86 | 3 | 5 | 0 | 5 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| patient_gender | 573 | 0.86 | 0.66 | 0.48 | 0.00 | 0.00 | 1.00 | 1.00 | 1.00 | ▅▁▁▁▇ |
| presc_thyroxine | 558 | 0.87 | 0.12 | 0.33 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| queried_why_on_thyroxine | 561 | 0.86 | 0.01 | 0.11 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| presc_anthyroid_meds | 557 | 0.87 | 0.01 | 0.10 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| sick | 566 | 0.86 | 0.04 | 0.19 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| pregnant | 568 | 0.86 | 0.01 | 0.12 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| thyroid_surgery | 565 | 0.86 | 0.01 | 0.12 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| radioactive_iodine_therapyI131 | 567 | 0.86 | 0.02 | 0.13 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| query_hypothyroid | 566 | 0.86 | 0.06 | 0.24 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| query_hyperthyroid | 568 | 0.86 | 0.06 | 0.24 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| lithium | 567 | 0.86 | 0.00 | 0.07 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| goitre | 563 | 0.86 | 0.01 | 0.10 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| tumor | 562 | 0.86 | 0.03 | 0.16 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| hypopituitarism | 567 | 0.86 | 0.00 | 0.02 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| psych_condition | 564 | 0.86 | 0.05 | 0.21 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| TSH_measured | 561 | 0.86 | 0.90 | 0.30 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| TSH_reading | 921 | 0.78 | 4.96 | 23.61 | 0.00 | 0.50 | 1.40 | 2.70 | 530.00 | ▇▁▁▁▁ |
| T3_measured | 562 | 0.86 | 0.80 | 0.40 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▂▁▁▁▇ |
| T3_reading | 1299 | 0.69 | 2.01 | 0.82 | 0.05 | 1.60 | 2.00 | 2.40 | 10.60 | ▇▅▁▁▁ |
| T4_measured | 564 | 0.86 | 0.94 | 0.24 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| T4_reading | 785 | 0.81 | 108.43 | 35.69 | 2.00 | 88.00 | 103.50 | 124.00 | 430.00 | ▃▇▁▁▁ |
| thyrox_util_rate_T4U_measured | 565 | 0.86 | 0.90 | 0.30 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| thyrox_util_rate_T4U_reading | 931 | 0.78 | 0.99 | 0.20 | 0.25 | 0.88 | 0.98 | 1.08 | 2.32 | ▁▇▃▁▁ |
| FTI_measured | 568 | 0.86 | 0.90 | 0.30 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| FTI_reading | 941 | 0.77 | 110.39 | 33.20 | 2.00 | 93.00 | 107.00 | 124.00 | 395.00 | ▁▇▁▁▁ |
یافتههای کلیدی از بررسی اولیه:
۱. ابعاد دیتاست:
۲. انواع متغیرها:
۳. مقادیر گمشده (Missing Values):
۴. توزیع دادهها:
⚠️ هشدارها و مسائل شناساییشده:
پیشپردازش یکی از مهمترین و زمانبرترین مراحل در هر پروژه دادهکاوی است. کیفیت مدل نهایی به شدت به کیفیت این مرحله وابسته است.
# حذف ستونهای Index و ستونهای بینام
df <- df %>%
select(-starts_with("X"), -contains("..."))
# بررسی ستونهای باقیمانده
ncol(df)## [1] 28
منطق حذف ستونها:
هنگام خروجی گرفتن از Excel یا سایر نرمافزارها، گاهی ستونهایی با نامهای
عجیب مانند X، X.1، … ایجاد میشوند
که:
تابع select() از dplyr به ما اجازه میدهد با استفاده از
الگوها (patterns) ستونها را انتخاب یا حذف کنیم:
starts_with(“X”): ستونهایی که با X شروع میشوند
contains(“…”): ستونهایی که حاوی … هستند
-) به معنی “حذف” است
# حذف رکوردهایی که برچسب ندارند
df <- df %>%
filter(!is.na(ThryroidClass))
# یکسانسازی متن (حروف بزرگ → کوچک)
df <- df %>%
mutate(ThryroidClass = factor(tolower(ThryroidClass)))
# بررسی سطوح (Levels)
levels(df$ThryroidClass)## [1] "negative" "sick"
##
## negative sick
## 3541 231
چرا این مرحله حیاتی است؟
۱. حذف رکوردهای بدون برچسب:
۲. یکسانسازی متن:
tolower() همه را به حروف کوچک تبدیل میکند
۳. تبدیل به Factor:
character و
factor
factor دارند
# حذف کاراکترهای غیرعددی (مانند کاما، فاصله)
df$patient_age <- as.numeric(gsub("[^0-9.]", "", df$patient_age))
# حذف سنهای غیرواقعی (بالای 120 سال)
df$patient_age[df$patient_age > 120] <- NA
# بررسی توزیع سن پس از پاکسازی
summary(df$patient_age)## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.00 36.00 54.00 51.64 67.00 94.00 195
hist(df$patient_age,
main = "توزیع سن پس از پاکسازی",
xlab = "سن (سال)",
col = "#0a9396",
breaks = 30)مراحل پاکسازی سن:
۱. حذف کاراکترهای غیرعددی:
gsub(): جستجو و جایگزینی در متن
[^0-9.]: به معنی “هر چیزی که عدد (0-9) یا نقطه (.)
نیست”
““ → حذف میشود
۲. تبدیل به عدد:
as.numeric() رشته تمیزشده را به عدد واقعی تبدیل میکند
۳. حذف سنهای غیرمنطقی:
# تابع محاسبه مد (پرتکرارترین مقدار)
getmode <- function(v) {
un <- unique(na.omit(v)) # مقادیر یکتا (بدون NA)
un[which.max(tabulate(match(v, un)))] # پرتکرارترین
}
# جایگزینی مقادیر گمشده
df <- df %>%
# برای متغیرهای عددی: میانه
mutate(across(where(is.numeric),
~ifelse(is.na(.), median(., na.rm = TRUE), .))) %>%
# برای متغیرهای متنی: مد
mutate(across(where(is.character),
~ifelse(is.na(.), getmode(.), .))) %>%
# برای متغیرهای فاکتور: مد
mutate(across(where(is.factor),
~ifelse(is.na(.), getmode(.), .)))
# بررسی: تعداد مقادیر گمشده باقیمانده
cat("تعداد مقادیر گمشده باقیمانده:", sum(is.na(df)), "\n")## تعداد مقادیر گمشده باقیمانده: 0
| Name | df |
| Number of rows | 3772 |
| Number of columns | 28 |
| _______________________ | |
| Column type frequency: | |
| character | 1 |
| numeric | 27 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| ref_src | 0 | 1 | 3 | 5 | 0 | 5 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| ThryroidClass | 0 | 1 | 1.06 | 0.24 | 1.00 | 1.00 | 1.00 | 1.00 | 2.00 | ▇▁▁▁▁ |
| patient_age | 0 | 1 | 51.76 | 18.49 | 1.00 | 37.00 | 54.00 | 66.00 | 94.00 | ▁▆▇▇▂ |
| patient_gender | 0 | 1 | 0.67 | 0.47 | 0.00 | 0.00 | 1.00 | 1.00 | 1.00 | ▃▁▁▁▇ |
| presc_thyroxine | 0 | 1 | 0.12 | 0.32 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| queried_why_on_thyroxine | 0 | 1 | 0.01 | 0.11 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| presc_anthyroid_meds | 0 | 1 | 0.01 | 0.10 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| sick | 0 | 1 | 0.04 | 0.19 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| pregnant | 0 | 1 | 0.01 | 0.12 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| thyroid_surgery | 0 | 1 | 0.01 | 0.11 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| radioactive_iodine_therapyI131 | 0 | 1 | 0.02 | 0.12 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| query_hypothyroid | 0 | 1 | 0.06 | 0.23 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| query_hyperthyroid | 0 | 1 | 0.06 | 0.24 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| lithium | 0 | 1 | 0.00 | 0.07 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| goitre | 0 | 1 | 0.01 | 0.09 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| tumor | 0 | 1 | 0.02 | 0.16 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| hypopituitarism | 0 | 1 | 0.00 | 0.02 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| psych_condition | 0 | 1 | 0.05 | 0.21 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | ▇▁▁▁▁ |
| TSH_measured | 0 | 1 | 0.91 | 0.29 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| TSH_reading | 0 | 1 | 4.45 | 21.88 | 0.00 | 0.61 | 1.40 | 2.40 | 530.00 | ▇▁▁▁▁ |
| T3_measured | 0 | 1 | 0.81 | 0.39 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▂▁▁▁▇ |
| T3_reading | 0 | 1 | 2.01 | 0.72 | 0.05 | 1.70 | 2.00 | 2.20 | 10.60 | ▇▃▁▁▁ |
| T4_measured | 0 | 1 | 0.94 | 0.23 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| T4_reading | 0 | 1 | 107.90 | 33.74 | 2.00 | 90.00 | 103.50 | 121.00 | 430.00 | ▂▇▁▁▁ |
| thyrox_util_rate_T4U_measured | 0 | 1 | 0.90 | 0.30 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| thyrox_util_rate_T4U_reading | 0 | 1 | 0.99 | 0.18 | 0.25 | 0.89 | 0.98 | 1.06 | 2.32 | ▁▇▂▁▁ |
| FTI_measured | 0 | 1 | 0.90 | 0.30 | 0.00 | 1.00 | 1.00 | 1.00 | 1.00 | ▁▁▁▁▇ |
| FTI_reading | 0 | 1 | 109.88 | 30.64 | 2.00 | 95.00 | 107.00 | 120.00 | 395.00 | ▁▇▁▁▁ |
استراتژی جایگزینی مقادیر گمشده (Imputation Strategy):
۱. برای متغیرهای عددی → استفاده از میانه (Median):
۲. برای متغیرهای دستهای → استفاده از مد (Mode):
۳. روشهای جایگزین (که اینجا استفاده نشد):
۴. محدودیتهای روش انتخابی:
تحلیل اکتشافی هدف دوگانه دارد: 1. درک عمیق از دادهها: الگوها، روابط، توزیعها 2. شناسایی مشکلات: پرتها، عدم تعادل، همخطی
class_counts <- table(df$ThryroidClass)
par(mfrow = c(1, 2))
# نمودار میلهای
barplot(class_counts,
main = "توزیع فراوانی کلاسها",
xlab = "وضعیت تیروئید",
ylab = "تعداد نمونه",
col = c("#0a9396", "#ee9b00"),
border = NA,
las = 1,
ylim = c(0, max(class_counts) * 1.1))
text(x = c(0.7, 1.9),
y = class_counts + 100,
labels = class_counts,
font = 2)
# نمودار دایرهای با درصدها
percentages <- round(100 * class_counts / sum(class_counts), 1)
pie(class_counts,
main = "توزیع درصدی کلاسها",
col = c("#0a9396", "#ee9b00"),
labels = paste0(names(class_counts), "\n", percentages, "%"),
cex = 1.2)⚠️ عدم تعادل شدید در دادهها (Severe Class Imbalance):
آمار دقیق:
چرا این مشکل است؟
راهکارهای مقابله:
۱. در سطح داده (Data-level):
۲. در سطح الگوریتم (Algorithm-level):
۳. در سطح ارزیابی (Evaluation-level):
در این پروژه: ما از Up-sampling در Cross-Validation استفاده خواهیم کرد.
par(mfrow = c(2, 2))
boxplot(TSH_reading ~ ThryroidClass, data = df,
main = "TSH (Thyroid Stimulating Hormone)",
xlab = "وضعیت",
ylab = "TSH (µU/mL)",
col = c("#0a9396", "#ee9b00"),
outline = FALSE) # حذف پرتها برای وضوح بیشتر
boxplot(T3_reading ~ ThryroidClass, data = df,
main = "T3 (Triiodothyronine)",
xlab = "وضعیت",
ylab = "T3 (ng/dL)",
col = c("#0a9396", "#ee9b00"),
outline = FALSE)
boxplot(T4_reading ~ ThryroidClass, data = df,
main = "T4 (Thyroxine)",
xlab = "وضعیت",
ylab = "T4 (µg/dL)",
col = c("#0a9396", "#ee9b00"),
outline = FALSE)
boxplot(FTI_reading ~ ThryroidClass, data = df,
main = "FTI (Free Thyroxine Index)",
xlab = "وضعیت",
ylab = "FTI",
col = c("#0a9396", "#ee9b00"),
outline = FALSE)par(mfrow = c(1, 1))
# محاسبه آمارهای توصیفی برای هر گروه
df %>%
group_by(ThryroidClass) %>%
summarise(
TSH_mean = mean(TSH_reading, na.rm = TRUE),
TSH_median = median(TSH_reading, na.rm = TRUE),
T3_mean = mean(T3_reading, na.rm = TRUE),
T4_mean = mean(T4_reading, na.rm = TRUE),
FTI_mean = mean(FTI_reading, na.rm = TRUE)
)تحلیل عمیق نمودارهای جعبهای و تفاوتهای بیولوژیکی:
۱. TSH (Thyroid Stimulating Hormone - هورمون محرک تیروئید):
تفسیر پزشکی:
۲. T3 (Triiodothyronine - ترییدوتیرونین):
۳. T4 (Thyroxine - تیروکسین):
۴. FTI (Free Thyroxine Index - شاخص تیروکسین آزاد):
نتیجهگیری کلیدی برای مدلسازی:
# انتخاب متغیرهای عددی کلیدی
numeric_vars <- df %>%
select(patient_age, TSH_reading, T3_reading, T4_reading, FTI_reading)
# محاسبه ماتریس همبستگی
corr_matrix <- cor(numeric_vars, use = "pairwise.complete.obs")
# مصورسازی
corrplot(corr_matrix,
method = "color", # نمایش با رنگ
type = "upper", # فقط نیمه بالایی
addCoef.col = "black", # نمایش اعداد
tl.col = "black", # رنگ برچسبها
tl.srt = 45, # زاویه برچسبها
title = "ماتریس همبستگی پیرسون",
mar = c(0,0,2,0),
number.cex = 0.8) # اندازه اعداد## patient_age TSH_reading T3_reading T4_reading FTI_reading
## patient_age 1.000 -0.060 -0.210 -0.041 0.057
## TSH_reading -0.060 1.000 -0.124 -0.243 -0.265
## T3_reading -0.210 -0.124 1.000 0.464 0.278
## T4_reading -0.041 -0.243 0.464 1.000 0.746
## FTI_reading 0.057 -0.265 0.278 0.746 1.000
تحلیل جامع ماتریس همبستگی:
۱. همبستگی مثبت قوی (r > 0.7):
۲. همبستگی منفی متوسط:
تفسیر فیزیولوژیکی (حلقه بازخورد منفی):
۳. سن (patient_age):
نتیجهگیری برای Feature Engineering:
این مرحله شامل تبدیلات نهایی برای ورود به الگوریتمهای یادگیری ماشین است.
set.seed(123) # برای تکرارپذیری
# تقسیم طبقهای (Stratified Split)
train_idx <- createDataPartition(df$ThryroidClass,
p = 0.7, # 70% آموزش
list = FALSE) # خروجی به صورت vector
train_data <- df[train_idx, ]
test_data <- df[-train_idx, ]
# بررسی ابعاد
cat("تعداد نمونههای آموزش:", nrow(train_data), "\n")## تعداد نمونههای آموزش: 2641
## تعداد نمونههای آزمون: 1131
cat("نسبت تقسیم:", round(nrow(train_data)/nrow(df), 2), "/",
round(nrow(test_data)/nrow(df), 2), "\n\n")## نسبت تقسیم: 0.7 / 0.3
## توزیع کلاس در آموزش:
##
## 1 2
## 0.93941689 0.06058311
##
## توزیع کلاس در آزمون:
##
## 1 2
## 0.9372237 0.0627763
اهمیت تقسیم صحیح دادهها:
۱. چرا تقسیم به آموزش/آزمون؟
۲. چرا Stratified Split؟
createDataPartition() از caret این کار را خودکار انجام
میدهد
۳. نسبت تقسیم (70/30):
۴. set.seed(123):
# One-Hot Encoding برای متغیرهای دستهای
dummies <- dummyVars(ThryroidClass ~ ., data = train_data)
train_x <- predict(dummies, newdata = train_data) %>% as.data.frame()
test_x <- predict(dummies, newdata = test_data) %>% as.data.frame()
# افزودن ستون هدف
train_x$ThryroidClass <- train_data$ThryroidClass
test_x$ThryroidClass <- test_data$ThryroidClass
cat("تعداد ویژگیها پس از One-Hot Encoding:", ncol(train_x) - 1, "\n")## تعداد ویژگیها پس از One-Hot Encoding: 31
# شناسایی ستونهای عددی
numeric_cols <- sapply(train_x, is.numeric)
# محاسبه پارامترهای نرمالسازی (فقط از train)
preproc <- preProcess(train_x[, numeric_cols],
method = c("center", "scale"))
# نرمالسازی
train_scaled <- train_x
test_scaled <- test_x
train_scaled[, numeric_cols] <- predict(preproc, train_x[, numeric_cols])
test_scaled[, numeric_cols] <- predict(preproc, test_x[, numeric_cols])
# بررسی نمونهای از نرمالسازی
cat("\nنمونه قبل از نرمالسازی:\n")##
## نمونه قبل از نرمالسازی:
## [1] 2.10 76.00 1.80 0.15 1.60
##
## نمونه بعد از نرمالسازی:
## [1] -0.1079503 3.1081318 -0.1210061 -0.1928131 -0.1297100
توضیح جامع One-Hot Encoding:
مسئله: اکثر الگوریتمهای ML فقط با اعداد کار میکنند، نه متن!
راهحل: One-Hot Encoding
</li>
چرا این کار را نمیکنیم؟ (Label Encoding)
نکته مهم: Dummy Variable Trap
dummyVars() خودکار K-1 ستون ایجاد میکند
توضیح جامع نرمالسازی (Standardization):
چرا نرمالسازی لازم است؟
فرمول نرمالسازی (Z-score):
⚠️ خطر Data Leakage:
preProcess() انجام میدهد
الگوریتمهایی که نیاز به نرمالسازی دارند:
# محاسبه فراوانی هر کلاس
class_counts <- table(train_scaled$ThryroidClass)
# محاسبه وزن: نسبت معکوس
class_weights <- max(class_counts) / class_counts
cat("فراوانی کلاسها:\n")## فراوانی کلاسها:
##
## -0.253900976999973 3.93705202460583
## 2481 160
##
## وزن کلاسها:
##
## -0.253900976999973 3.93705202460583
## 1.00000 15.50625
##
## تفسیر: کلاس اقلیت (Sick) وزن 15.51 برابر کلاس اکثریت دارد
منطق وزندهی کلاسها (Class Weighting):
هدف: جبران عدم تعادل بدون تغییر دادههای اصلی
نحوه محاسبه:
تأثیر در الگوریتم:
مقایسه با Up-sampling:
در این پروژه: ما از Up-sampling در Cross-Validation
استفاده میکنیم (پارامتر sampling=“up” در
trainControl)
در این بخش، چهار الگوریتم مختلف یادگیری ماشین را آموزش داده و مقایسه میکنیم.
ctrl <- trainControl(
method = "cv", # Cross-Validation
number = 10, # 10-Fold
classProbs = TRUE, # محاسبه احتمالات کلاس
summaryFunction = twoClassSummary,# معیارهای ارزیابی برای 2 کلاس
savePredictions = "final", # ذخیره پیشبینیهای نهایی
sampling = "up" # Up-sampling برای رفع عدم تعادل
)درک عمیق Cross-Validation:
مسئله: چگونه عملکرد مدل را بدون اتلاف دادههای تست ارزیابی کنیم؟
راهحل: K-Fold Cross-Validation
مزایا:
انتخاب K:
پارامتر sampling=“up”:
classProbs=TRUE:
set.seed(123)
# تبدیل متغیر هدف به فرمت مناسب
train_scaled$ThryroidClass <- as.factor(train_scaled$ThryroidClass)
levels(train_scaled$ThryroidClass) <- make.names(levels(train_scaled$ThryroidClass))
# آموزش مدل
knn_fit <- train(
ThryroidClass ~ .,
data = train_scaled,
method = "knn",
trControl = ctrl,
metric = "ROC", # معیار بهینهسازی
tuneLength = 15 # آزمایش 15 مقدار مختلف برای K
)
print(knn_fit)## k-Nearest Neighbors
##
## 2641 samples
## 31 predictor
## 2 classes: 'X.0.253900976999973', 'X3.93705202460583'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ...
## Addtional sampling using up-sampling
##
## Resampling results across tuning parameters:
##
## k ROC Sens Spec
## 5 0.8692016 0.9218147 0.78750
## 7 0.8998833 0.8980389 0.85625
## 9 0.9012579 0.8746615 0.86875
## 11 0.9077472 0.8524987 0.88125
## 13 0.9125876 0.8395955 0.90000
## 15 0.9139335 0.8190374 0.90000
## 17 0.9165348 0.8097697 0.90625
## 19 0.9165544 0.8113778 0.90000
## 21 0.9177137 0.8069520 0.90625
## 23 0.9203490 0.8105762 0.91250
## 25 0.9212438 0.8339519 0.88125
## 27 0.9206529 0.8149987 0.87500
## 29 0.9308423 0.8404003 0.90000
## 31 0.9285006 0.8331520 0.86875
## 33 0.9311951 0.8549035 0.85625
##
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was k = 33.
نتایج مدل KNN:
تفسیر:
نمودار K vs ROC:
نحوه کار الگوریتم KNN:
مرحله پیشبینی:
معیار فاصله (معمولاً اقلیدسی):
تنظیم K:
مزایا و معایب KNN:
set.seed(123)
tree_fit <- train(
ThryroidClass ~ .,
data = train_scaled,
method = "rpart",
trControl = ctrl,
metric = "ROC",
tuneLength = 10 # آزمایش 10 مقدار مختلف برای cp
)
print(tree_fit)## CART
##
## 2641 samples
## 31 predictor
## 2 classes: 'X.0.253900976999973', 'X3.93705202460583'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ...
## Addtional sampling using up-sampling
##
## Resampling results across tuning parameters:
##
## cp ROC Sens Spec
## 0.00000000 0.9037089 0.9709839 0.83125
## 0.04722222 0.9076815 0.9601066 0.84375
## 0.09444444 0.8996041 0.9742081 0.82500
## 0.14166667 0.8996041 0.9742081 0.82500
## 0.18888889 0.8996041 0.9742081 0.82500
## 0.23611111 0.8996041 0.9742081 0.82500
## 0.28333333 0.8996041 0.9742081 0.82500
## 0.33055556 0.8996041 0.9742081 0.82500
## 0.37777778 0.8996041 0.9742081 0.82500
## 0.42500000 0.8996041 0.9742081 0.82500
##
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.04722222.
rpart.plot(tree_fit$finalModel,
type = 2,
extra = 104,
roundint = FALSE,
main = "درخت تصمیم تشخیص تیروئید",
box.palette = "GnBu")نتایج درخت تصمیم:
تفسیر درخت:
نقاط قوت:
نقاط ضعف:
نحوه کار الگوریتم درخت تصمیم (CART):
مرحله ساخت (Growing):
</li>
<li>تقسیمی که بیشترین کاهش Gini را دارد → انتخاب میشود</li>
مثال محاسبه Gini:
هرس کردن (Pruning):
مزایا و معایب:
set.seed(123)
rf_fit <- randomForest(
ThryroidClass ~ .,
data = train_scaled,
ntree = 200, # 200 درخت
importance = TRUE, # محاسبه اهمیت متغیرها
strata = train_scaled$ThryroidClass, # نمونهبرداری طبقهای
sampsize = rep(min(table(train_scaled$ThryroidClass)), 2) # تعادل کلاسها
)
print(rf_fit)##
## Call:
## randomForest(formula = ThryroidClass ~ ., data = train_scaled, ntree = 200, importance = TRUE, strata = train_scaled$ThryroidClass, sampsize = rep(min(table(train_scaled$ThryroidClass)), 2))
## Type of random forest: classification
## Number of trees: 200
## No. of variables tried at each split: 5
##
## OOB estimate of error rate: 4.05%
## Confusion matrix:
## X.0.253900976999973 X3.93705202460583 class.error
## X.0.253900976999973 2392 89 0.03587263
## X3.93705202460583 18 142 0.11250000
نتایج جنگل تصادفی:
متغیرهای مهم (به ترتیب اولویت):
تفسیر نمودار اهمیت:
نتیجهگیری:
نحوه کار الگوریتم Random Forest:
مفهوم Ensemble Learning:
مراحل ساخت Random Forest:
OOB Error (Out-of-Bag):
چرا Random Forest بهتر از درخت تصمیم تک است؟
پارامترهای کلیدی:
مزایا و معایب:
set.seed(123)
train_scaled$ThryroidClass <- as.factor(train_scaled$ThryroidClass)
nn_fit <- train(
ThryroidClass ~ .,
data = train_scaled,
method = "nnet",
trControl = ctrl,
metric = "ROC",
tuneLength = 5, # آزمایش 5 ترکیب مختلف
trace = FALSE # عدم نمایش خروجیهای میانی
)
print(nn_fit)## Neural Network
##
## 2641 samples
## 31 predictor
## 2 classes: 'X.0.253900976999973', 'X3.93705202460583'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ...
## Addtional sampling using up-sampling
##
## Resampling results across tuning parameters:
##
## size decay ROC Sens Spec
## 1 0e+00 0.8617393 0.8226584 0.84375
## 1 1e-04 0.8719241 0.8286987 0.85625
## 1 1e-03 0.8818816 0.8464309 0.83750
## 1 1e-02 0.8884843 0.8303083 0.86250
## 1 1e-01 0.9146396 0.8823245 0.83125
## 3 0e+00 0.9342339 0.9234114 0.80000
## 3 1e-04 0.8906728 0.9246211 0.75000
## 3 1e-03 0.9147312 0.9085115 0.76875
## 3 1e-02 0.9112299 0.9069261 0.81875
## 3 1e-01 0.9373114 0.9157922 0.80000
## 5 0e+00 0.9051054 0.9496227 0.74375
## 5 1e-04 0.9144806 0.9439775 0.73750
## 5 1e-03 0.9061536 0.9564775 0.72500
## 5 1e-02 0.9123274 0.9488227 0.72500
## 5 1e-01 0.9250364 0.9399598 0.76875
## 7 0e+00 0.8992082 0.9540565 0.72500
## 7 1e-04 0.9253039 0.9548695 0.76250
## 7 1e-03 0.9183645 0.9576904 0.73125
## 7 1e-02 0.9185217 0.9524453 0.73125
## 7 1e-01 0.9242505 0.9552662 0.77500
## 9 0e+00 0.9188203 0.9617389 0.65625
## 9 1e-04 0.9048643 0.9556824 0.67500
## 9 1e-03 0.8934085 0.9604984 0.70625
## 9 1e-02 0.9239882 0.9560808 0.72500
## 9 1e-01 0.9292169 0.9528469 0.73750
##
## ROC was used to select the optimal model using the largest value.
## The final values used for the model were size = 3 and decay = 0.1.
نتایج شبکه عصبی:
تفسیر:
نحوه کار شبکه عصبی:
ساختار شبکه Feed-forward:
الگوریتم آموزش (Backpropagation):
چالشها:
چرا در اینجا عملکرد ضعیفتر است؟
fix_levels_safe <- function(x) {
nums <- as.numeric(x)
nums <- round(nums)
nums[nums < 1] <- 1
nums[nums > 2] <- 2
factor(nums, levels = c(1, 2), labels = c("Negative", "Sick"))
}
actual_clean <- fix_levels_safe(test_scaled$ThryroidClass)
raw_knn <- predict(knn_fit, test_scaled, type = "raw")
p_knn_final <- fix_levels_safe(raw_knn)
raw_tree <- predict(tree_fit, test_scaled, type = "raw")
p_tree_final <- fix_levels_safe(raw_tree)
raw_rf <- predict(rf_fit, test_scaled, type = "response")
p_rf_final <- fix_levels_safe(raw_rf)
raw_nn <- tryCatch({
predict(nn_fit, test_scaled, type = "raw")
}, error = function(e) {
predict(nn_fit, test_scaled, type = "response")
})
p_nn_final <- fix_levels_safe(raw_nn)
eval_model <- function(pred, actual, model_name) {
cm <- confusionMatrix(pred, actual, mode = "everything", positive = "Sick")
data.frame(
Model = model_name,
Accuracy = round(cm$overall['Accuracy'], 4),
Sensitivity = round(cm$byClass['Sensitivity'], 4),
Specificity = round(cm$byClass['Specificity'], 4),
F1_Score = round(cm$byClass['F1'], 4)
)
}
results <- rbind(
eval_model(p_knn_final, actual_clean, "KNN"),
eval_model(p_tree_final, actual_clean, "Decision Tree"),
eval_model(p_rf_final, actual_clean, "Random Forest"),
eval_model(p_nn_final, actual_clean, "Neural Network")
)
knitr::kable(results, caption = "جدول نهایی مقایسه عملکرد مدلها")| Model | Accuracy | Sensitivity | Specificity | F1_Score | |
|---|---|---|---|---|---|
| Accuracy | KNN | 0.8691 | 0.8451 | 0.8708 | 0.4478 |
| Accuracy1 | Decision Tree | 0.9390 | 0.9437 | 0.9387 | 0.6601 |
| Accuracy2 | Random Forest | 0.9646 | 0.9718 | 0.9642 | 0.7753 |
| Accuracy3 | Neural Network | 0.8939 | 0.8592 | 0.8962 | 0.5041 |
##
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
##
## smiths
results_long <- melt(results, id.vars = "Model")
ggplot(results_long, aes(x = Model, y = value, fill = variable)) +
geom_bar(stat = "identity", position = "dodge", width = 0.7) +
labs(title = "مقایسه جامع عملکرد مدلها",
x = "مدل",
y = "نمره",
fill = "معیار ارزیابی") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 11),
legend.position = "top") +
scale_fill_brewer(palette = "Set1") +
geom_hline(yintercept = 0.9, linetype = "dashed", color = "red", alpha = 0.5) +
annotate("text", x = 4.5, y = 0.92, label = "آستانه قبولی (90%)",
color = "red", size = 3)## Confusion Matrix and Statistics
##
## Reference
## Prediction Negative Sick
## Negative 1022 2
## Sick 38 69
##
## Accuracy : 0.9646
## 95% CI : (0.9521, 0.9746)
## No Information Rate : 0.9372
## P-Value [Acc > NIR] : 2.793e-05
##
## Kappa : 0.7569
##
## Mcnemar's Test P-Value : 3.130e-08
##
## Sensitivity : 0.97183
## Specificity : 0.96415
## Pos Pred Value : 0.64486
## Neg Pred Value : 0.99805
## Prevalence : 0.06278
## Detection Rate : 0.06101
## Detection Prevalence : 0.09461
## Balanced Accuracy : 0.96799
##
## 'Positive' Class : Sick
##
جدول خلاصه نتایج:
| مدل | دقت | حساسیت | ویژگی | F1-Score | رتبه |
|---|---|---|---|---|---|
| جنگل تصادفی | 96.5% | 97.2% | 96.3% | 0.7750 | 🥇 اول |
| درخت تصمیم | 93.9% | 94.4% | 93.6% | 0.6600 | 🥈 دوم |
| شبکه عصبی | 94.9% | 73.2% | 96.3% | 0.6420 | 🥉 سوم |
| KNN | 86.9% | 84.5% | 87.2% | 0.4480 | چهارم |
تفسیر ماتریس درهمریختگی (Random Forest):
Reference
Prediction Negative Sick
Negative 1022 2
Sick 38 69
محاسبه معیارها:
🏆 مدل برنده: جنگل تصادفی (Random Forest)
دلایل انتخاب به عنوان مدل نهایی:
مقایسه با سایر مدلها:
یافتههای علمی و پزشکی:
</li>
<li><strong>T3 (Triiodothyronine):</strong> شاخص فعالیت متابولیک</li>
<li><strong>T4 و FTI:</strong> نشانگرهای تکمیلی</li>
توصیههای کاربردی برای پیادهسازی:
saveRDS(rf_fit, “thyroid_model.rds”)
model <-
readRDS(“thyroid_model.rds”)
</li>
</li>
مسیرهای بهبود در آینده:
ملاحظات اخلاقی و قانونی: